package com.example.dayu521.sg_oss.conf;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.dayu521.sg_oss.domain.ServiceNode;
import com.example.dayu521.sg_oss.service.ServiceNodeService;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import io.lettuce.core.codec.CRC16;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

@Slf4j
@Configuration
@EnableConfigurationProperties(RedisC.ClusterConfigurationProperties.class)
public class RedisC {

    public static ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    private static ObjectMapper objectMapper;

    static {
        ObjectMapper b = new ObjectMapper();
        //默认转long类型
        //fixme 注意这个会被下面的配置覆盖,导致不生效 https://segmentfault.com/q/1010000043690114
        b.enable(DeserializationFeature.USE_LONG_FOR_INTS);

        //https://zhuanlan.zhihu.com/p/148753594
        b.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).build()
                , ObjectMapper.DefaultTyping.NON_FINAL
                , JsonTypeInfo.As.WRAPPER_ARRAY);
        objectMapper = b;


    }

    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //GenericJackson2JsonRedisSerializer
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(objectMapper, Object.class);
        template.setKeySerializer(RedisSerializer.string());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(RedisSerializer.string());
        template.setHashValueSerializer(serializer);
        return template;
    }

    @ConfigurationProperties(prefix = "sg.num.redis-cluster")
    @Data
    public static class ClusterConfigurationProperties {

        /*
         * spring.redis.cluster.nodes[0] = 127.0.0.1:7379
         * spring.redis.cluster.nodes[1] = 127.0.0.1:7380
         * ...
         */
        private List<String> nodes;
        private Integer maxRedirects;

    }

    @Configuration
    @ConditionalOnProperty(prefix = "sg.num",name = "backend",havingValue = "redis-cluster")
    public static class ClusterConf{

        private final static Pattern compile = Pattern.compile("""
            ^\\{(\\d+)}\\.(.+)""");

        @Bean("redisTemplateCluster")
        public RedisTemplate<Object, Object> redisTemplate2(ClusterConfigurationProperties clusterConfigurationProperties) {

            RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterConfigurationProperties.getNodes());
            redisClusterConfiguration.setMaxRedirects(clusterConfigurationProperties.getMaxRedirects());

            var redisConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration);
            redisConnectionFactory.afterPropertiesSet();

            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            //GenericJackson2JsonRedisSerializer
            Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(objectMapper, Object.class);
            template.setKeySerializer(RedisSerializer.string());
            template.setValueSerializer(serializer);
            template.setHashKeySerializer(RedisSerializer.string());
            template.setHashValueSerializer(serializer);

            return template;
        }

        @Autowired
        private ServiceNodeService service;

        @Bean
        public Map<Integer,Integer> init_node_map(@Qualifier("redisTemplateCluster") RedisTemplate<Object, Object> template) {

            //https://redis.io/docs/reference/cluster-spec/#appendix-a-crc16-reference-implementation-in-ansi-c
//        for(var i: Arrays.asList(0,5461,10922)) {
//            byte[] buf = String.valueOf(i).getBytes();
//            log.info("crc16 {} -> {}",i, CRC16.crc16(buf)%16384);
//        }

            var res=new HashMap<Integer,Integer>();

            Function<RedisClusterNode,Integer> dox= i->{
                var s=i.getSlotRange().getSlots();
                if(s!=null&&!s.isEmpty()){
                    Random random = new Random();
                    var max_cc=20;
                    var key=0;
                    boolean find=false;
                    while (max_cc>0){
                        key=random.nextInt(16384);
                        //也可以使用CLUSTER KEYSLOT key https://redis.io/commands/cluster-keyslot/
                        if(s.contains(CRC16.crc16(String.valueOf(key).getBytes())%16384)){
                            find=true;
                            break;
                        }
                        max_cc--;
                    }
                    if(!find){
                        throw new RuntimeException("随机生成key,无法匹配节点对应的槽.请指定key或者增加尝试次数或者多启动几次");
                    }
                    Long nodeId = get_node_id(service);
                    template.opsForValue().setIfAbsent(String.format("{%d}.node_id", key), nodeId);
                    return key;
                }
                return null;
            };
            var res_key=new AtomicInteger(0);
            template.getConnectionFactory().getClusterConnection().clusterGetNodes().forEach(i -> {
                if (i.isMaster()&&i.isConnected()) {
                    //https://redis.io/commands/keys/
                    //fixme 在keys不存在时的并发启动问题
                    Set<Object> keys = template.opsForCluster().keys(i, "{*}.node_id");
                    if(keys.isEmpty()){
                        template.opsForCluster().flushDb(i);
                        var ke=dox.apply(i);
                        res.put(res_key.getAndIncrement(), ke);
                        log.info("{} 初始化 node_id",i.getId());
                    }else if(keys.size()==1){
                        var m=compile.matcher(keys.stream().findAny().get().toString());
                        m.matches();
                        String group = m.group(1);
                        Integer value = Integer.valueOf(group);
                        res.put(res_key.getAndIncrement(), value);
                        log.info("init_node_map 增加条目");
                    }else{
                        throw new RuntimeException("发现多个node_id,请检查数据一致性");
                    }
                }
            });

            return res;
        }
    }

    @ConfigurationProperties(prefix = "sg.num.redis")
    @Data
    public static class SgNumRedisCP {
        @Data
        private final static class RNode {
            private String host;
            private String port;
        }

        private List<RNode> nodes;
    }
    @Configuration
    @ConditionalOnProperty(prefix = "sg.num",name = "backend",havingValue = "redis")
    @EnableConfigurationProperties(SgNumRedisCP.class)
    public static class NoClusterConf{

        @Autowired
        private ServiceNodeService service;

        @Bean
        public Map<Long,RedisTemplate<String,Object>> redisTemplate_map(SgNumRedisCP sgNumRedisCP){
            var map = new HashMap<Long,RedisTemplate<String,Object>>();
            AtomicLong index=new AtomicLong(0);
            sgNumRedisCP.getNodes().forEach(node->{
                var redisStandaloneConfiguration = new RedisStandaloneConfiguration();
                redisStandaloneConfiguration.setHostName(node.host);
                redisStandaloneConfiguration.setPassword(node.port);
                redisStandaloneConfiguration.setDatabase(15);
                var lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
                lettuceConnectionFactory.afterPropertiesSet();

//                ObjectMapper b = new ObjectMapper();
//                //默认转long类型
//                b.enable(DeserializationFeature.USE_LONG_FOR_INTS);
                Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(objectMapper, Object.class);

                RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
                redisTemplate.setConnectionFactory(lettuceConnectionFactory);
                redisTemplate.setKeySerializer(RedisSerializer.string());
                redisTemplate.setValueSerializer(serializer);
                redisTemplate.afterPropertiesSet();

                Long node_id=null;

                var node_id_str=redisTemplate.opsForValue().get("node_id");

                if(node_id_str!=null){
                    node_id = Long.valueOf((Integer)node_id_str);
                }

                if(node_id==null){
                    node_id=get_node_id(service);
                    redisTemplate.opsForValue().setIfAbsent("node_id",node_id);
                    log.info("{}:{} 初始化redis:node_id成功",node.host,node.port);
                }
                map.put(node_id,redisTemplate);
                index.incrementAndGet();
            });

            return map;
        }
    }

    private static Long get_node_id(ServiceNodeService service){
        var i=0;
        var init_ok=false;
        var res=0l;
        while (i<5) {
            var r = service.getOne(new QueryWrapper<ServiceNode>().eq("first_key", "redis:node_id"), true);
            r.setAcc(r.getAcc() + 1);
            if(service.updateById(r)){
                init_ok=true;
                res=r.getAcc();
                break;
            }
            i++;
        }
        if(!init_ok){
            throw new RuntimeException("尝试最大次数后,初始化redis:node失败.请稍后重启再试");
        }
        return res;
    }

}
